﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Text.RegularExpressions;
using System.Net.Sockets;
using System.IO;
using System.IO.Compression;
using NeoFramework.Net;

namespace NeoFramework
{

    public class HTTPWrapper
    {
        /** Defines the type of request **/
        public enum Type { GET, POST }

        /** Defines static entities used in a HTTP Request **/
        public String HTTPVersion = "HTTP/1.1";
        public String newline = "\r\n";

        /** Variables for holding the returned headers and HTML **/
        public Hashtable returnHeaders = new Hashtable();
        public String HTML;

        /** Holds the HTTP Request header **/
        public Hashtable headers = new Hashtable();

        /** Regex for matching URLs and Headers values **/
        public Regex regURL;
        public Regex regHeaders;

        /** Holds all stored cookies **/
        public CookieJar cJar = new CookieJar();

        /** Initialization function **/
        public HTTPWrapper()
        {
            /** Define the static values for the HTTP Request. These can be modified as needed **/
            this.headers.Add("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-US; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7");
            this.headers.Add("Accept", "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5");
            this.headers.Add("Accept-Language", "en-us,en;q=0.5");
            this.headers.Add("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7");
            this.headers.Add("Accept-Encoding", "gzip, deflate");

            /** Regex strings for matching URLs and Header values **/
            this.regURL = new Regex("(.*?\\.\\w+)\\/(.*?)$", RegexOptions.IgnoreCase);
            this.regHeaders = new Regex("(.*?): (.*?)$", RegexOptions.IgnoreCase);
        }

        /** Core requests function **/
        public String request(Type RType, String URL, Hashtable pData, Hashtable Cheaders)
        {
            String host = "";
            String path = "";
            String retData = "";
            String retHeaders = "";
            String request = RType == Type.GET ? "GET" : "POST";

            Byte[] headers;

            TcpClient client;

            this.HTML = "";
            this.returnHeaders = new Hashtable();

            /** Don't need this part **/
            URL = URL.Replace("http://", "");

            /** Make sure this URL is valid **/
            if (!this.regURL.IsMatch(URL))
            {
                throw new Exception("Invalid URL given");
            }

            /** Match the URL to extract the host and path **/
            Match mURL = this.regURL.Match(URL);
            host = mURL.Groups[1].ToString();
            path = "/" + mURL.Groups[2].ToString();

            /** Build and encode the HTTP Request header **/
            headers = System.Text.Encoding.ASCII.GetBytes(this.buildHeaders(RType, host, path, pData, Cheaders));

            /** Open a new TCP connection with the host on the default HTTP port **/
            client = new TcpClient(host, 80);

            /** Create a Network Stream to send the HTTP Request header to the host **/
            NetworkStream nStream = client.GetStream();
            nStream.Write(headers, 0, headers.Length);

            /** Create a Stream Reader to read back the response from the host **/
            StreamReader sReader = new StreamReader(nStream, Encoding.Default);
            retData = sReader.ReadToEnd();

            /** Split the returned data by two new lines. By default returned HTTP responses have the returned data
             * and returned headers separated by two new lines **/
            String[] dParts = Regex.Split(retData, this.newline + this.newline);
            retHeaders = dParts[0];
            String HTML = dParts[1];

            /** Run through all the header values in the HTTP Response header **/
            foreach (String line in Regex.Split(retHeaders, this.newline))
            {
                /** Matches the header key and value in this format: {headerkey}: {headervalue} **/
                Match m = this.regHeaders.Match(line);

                /** If the current header key has been stored before we need to hold on to it, make a new array, then
                 * add it along with the new value to the array **/
                if (this.returnHeaders.ContainsKey(m.Groups[1].ToString()))
                {
                    /** If an array hasn't been made, need to make it **/
                    if (this.returnHeaders[m.Groups[1].ToString()].GetType().IsAssignableFrom(typeof(System.String)))
                    {
                        /** Grab the previous value and create an array **/
                        String tmpValue = this.returnHeaders[m.Groups[1].ToString()].ToString();
                        String[] tmpArray = new String[2];

                        /** Store the previous and new value in the array **/
                        tmpArray[0] = tmpValue;
                        tmpArray[1] = m.Groups[2].ToString();

                        /** Set the header value for this key to the newly created array **/
                        this.returnHeaders[m.Groups[1].ToString()] = tmpArray;
                    }
                    else
                    {
                        /** If an array has already been previously created just add the new value to it **/
                        String[] tmpArray = (String[])this.returnHeaders[m.Groups[1].ToString()];
                        ICollection<String> tmpList = new List<String>(tmpArray);

                        tmpList.Add(m.Groups[2].ToString());
                        this.returnHeaders[m.Groups[1].ToString()] = tmpList.ToArray<String>();
                    }
                }
                else
                {
                    this.returnHeaders[m.Groups[1].ToString()] = m.Groups[2].ToString();
                }
            }

            /** If there's cookies being sent, we need to store them **/
            if (this.returnHeaders.ContainsKey("Set-Cookie"))
            {
                /** If it's just one cookie, and not an array of cookies, make a new array, store it, and send it off **/
                if (this.returnHeaders["Set-Cookie"].GetType().IsAssignableFrom(typeof(System.String)))
                {
                    String[] tmpArray = new String[0];
                    tmpArray[0] = this.returnHeaders["Set-Cookie"].ToString();
                    this.cJar.parseCookiesRaw(tmpArray, host);
                }
                else
                {
                    /** Otherwise just send the array of cookies off **/
                    String[] tmpArray = (String[])this.returnHeaders["Set-Cookie"];
                    this.cJar.parseCookiesRaw(tmpArray, host);
                }
            }

            /** If the returned content is encoded in GZIP, make sure to decompress it **/
            if (this.returnHeaders["Content-Encoding"].ToString() == "gzip")
            {
                HTML = this.DecompressGzip(HTML);
            }

            return HTML;
        }

        /** Set whether or not to allow GZIP encoding **/
        public void setUseGZIP(Boolean state)
        {
            if (state == true)
            {
                this.headers.Add("Accept-Encoding", "gzip, deflate");
            }
            else
            {
                if (this.headers.Contains("Accept-Encoding"))
                {
                    this.headers.Remove("Accept-Encoding");
                }
            }
        }

        /** Builds HTTP Request headers **/
        private String buildHeaders(Type RType, String host, String path, Hashtable pData, Hashtable Cheaders)
        {
            String headers = "";
            String request = RType == Type.GET ? "GET" : "POST";

            headers += request + " " + path + " " + this.HTTPVersion + this.newline;
            headers += "Host: " + host + this.newline;

            foreach (String key in Cheaders.Keys)
            {
                this.headers[key] = Cheaders[key];
            }

            foreach (String key in this.headers.Keys)
            {
                headers += key + ": " + this.headers[key] + this.newline;
            }

            if (this.cJar.hasCookiesForDomain(host))
            {
                headers += "Cookie: " + this.cJar.cookiesToString(this.cJar.getCookiesByDomain(host)) + this.newline;
            }

            if (RType == Type.POST)
            {
                String postData = "";

                foreach (String key in pData.Keys)
                {
                    postData += key + "=" + pData[key].ToString() + "&";
                }

                postData = postData.Substring(0, postData.Length - 1);

                headers += "Content-Type: application/x-www-form-urlencoded" + this.newline;
                headers += "Content-Length: " + postData.Length.ToString() + this.newline + this.newline;
                headers += postData;

            }
            else
            {
                headers += this.newline;
            }

            return headers;
        }

        /** Decompresses GZIP encoded data **/
        private String DecompressGzip(string compressed)
        {
            MemoryStream memStream = new MemoryStream(System.Text.Encoding.Default.GetBytes(compressed));
            GZipStream decompressStream = new GZipStream(memStream, CompressionMode.Decompress);

            byte[] endBytes = new byte[4];
            int position = (int)memStream.Length - 4;

            memStream.Position = position;
            memStream.Read(endBytes, 0, 4);
            memStream.Position = 0;

            byte[] buffer = new byte[BitConverter.ToInt32(endBytes, 0) + 100];
            int offset = 0;
            int total = 0;

            while (true)
            {
                int o = decompressStream.Read(buffer, offset, 100);
                if (o == 0) break;
                offset += o;
                total += o;
            }

            return Encoding.ASCII.GetString(buffer);
        }
    }
}